/*
 * Copyright (C) Michael Martin & Antonio Carrozzini (TBJD)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.tbjd.client.managers;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.tbjd.client.beans.Task;
import org.tbjd.client.constants.NotificationsConstants;
import org.tbjd.client.constants.TimeConstants;
import org.tbjd.client.exceptions.DateManagerException;
import org.tbjd.client.states.StateBeginFrozenASAP;
import org.tbjd.client.states.StateBeginFrozenSPEC;
import org.tbjd.client.states.StateEndFrozenASAP;
import org.tbjd.client.states.StateEndFrozenSPEC;
import org.tbjd.client.utils.TimeUtils;

public class TaskManager {

	private List<Task> lstBeans = null;
	long offset = 0;

	/**
	 * <p>
	 * According to the changed value of the received {@link org.tbjd.client.beans.Task task}, compute the other related values
	 * </p>
	 * 
	 * @param map
	 *            contains the changed value (key: DATEMANAGER_ACTION_[property]), the associated bean (key: TASKMANAGER_BEAN) and the sender of the
	 *            action (key: TASKMANAGER_SENDER)
	 * @return List<Task> all changed beans
	 */
	public List<Task> manageDates(Map<Integer, Object> map) throws DateManagerException {

		lstBeans = new ArrayList<Task>();

		Task task = (Task) map.get(NotificationsConstants.TASKMANAGER_BEAN);

		Date originalBeginDate = task.getBeginDate();
		Date originalEndDate = task.getEndDate();

		if (map.containsKey(NotificationsConstants.TASKMANAGER_ACTION_beginDate)) {
			Date newBeginDate = (Date) map.get(NotificationsConstants.TASKMANAGER_ACTION_beginDate);

			// if task is a phase
			if (task.getLstChildren() != null && task.getLstChildren().size() > 0) {
				offset = TimeUtils.getOffset(originalBeginDate, newBeginDate);

				// shift it
				shiftPhase(task, offset);

			} else {

				// change appropriate property
				task.getState().beginDateChangedTemplate(task, newBeginDate);

				// update successors and phase's dates and duration
				updateSuccessorsAndPhase(task, originalEndDate);

			}

		} else if (map.containsKey(NotificationsConstants.TASKMANAGER_ACTION_endDate)) {
			Date newEndDate = (Date) map.get(NotificationsConstants.TASKMANAGER_ACTION_endDate);

			// if task is a phase
			if (task.getLstChildren() != null && task.getLstChildren().size() > 0) {
				offset = TimeUtils.getOffset(originalEndDate, newEndDate);

				// shift it
				shiftPhase(task, offset);

			} else {

				// change appropriate property
				task.getState().endDateChangedTemplate(task, newEndDate);

				// update successors and phase's dates and duration
				updateSuccessorsAndPhase(task, originalEndDate);

			}

		} else if (map.containsKey(NotificationsConstants.TASKMANAGER_ACTION_duration)) {
			Float newDuration = (Float) map.get(NotificationsConstants.TASKMANAGER_ACTION_duration);

			// change appropriate property
			task.getState().durationChangedTemplate(task, newDuration);

			// update successors and phase's dates and duration
			updateSuccessorsAndPhase(task, originalEndDate);

		} else if (map.containsKey(NotificationsConstants.TASKMANAGER_ACTION_effort)) {
			Float newEffort = (Float) map.get(NotificationsConstants.TASKMANAGER_ACTION_effort);

			// change appropriate property
			task.getState().effortChangedTemplate(task, newEffort);

			// update successors and phase's dates and duration
			updateSuccessorsAndPhase(task, originalEndDate);

		} else if (map.containsKey(NotificationsConstants.TASKMANAGER_ACTION_resource)) {

			// change appropriate property
			task.getState().resourceChangedTemplate(task);

			// update successors and phase's dates and duration
			updateSuccessorsAndPhase(task, originalEndDate);

		}

		return lstBeans;
	}

	/**
	 * @param task
	 * @param originalEndDate
	 */
	private void updateSuccessorsAndPhase(Task task, Date originalEndDate) {
		lstBeans.add(task);

		offset = TimeUtils.getOffset(originalEndDate, task.getEndDate());

		// shift every successor
		for (Task successor : task.getSuccessors()) {
			if (successor.getState().isASAPChainingMode()) {
				shiftTask(successor, offset);
			}
		}

		// update phase dates and duration
		if (task.getParent() != null) {
			computePhaseDatesAndDuration(task.getParent().getLstChildren(), task.getParent());
		}
	}

	/**
	 * <p>
	 * Updates every child of the given task (parent) with the given offset
	 * </p>
	 * <p>
	 * Changes the State of the child if it has at least one external predecessor in ASAP mode
	 * </p>
	 * 
	 * @param phase
	 *            the task containing child to be updated
	 * @param offset
	 *            the amount of time in milliseconds to offset children
	 */
	protected void shiftPhase(Task phase, long offset) {
		// shift phase
		phase.getState().shift(phase, offset);
		lstBeans.add(phase);

		// for each child of the phase
		for (Task eachChild : phase.getLstChildren()) {

			// if the child has predecessors
			if (eachChild.getPredecessors() != null) {

				// for each predecessor
				for (Task eachPredecessor : eachChild.getPredecessors()) {

					// if predecessor is not in the same phase AND is in ASAP ChainingMode
					if (((eachPredecessor.getParent() != null && !eachPredecessor.getParent().equals(eachChild.getParent())) || eachPredecessor
							.getParent() == null)
							&& eachPredecessor.getState().isASAPChainingMode()) {

						// set ChainingMode to Manual
						if (eachChild.getState() instanceof StateBeginFrozenASAP) {
							eachChild.setState(new StateBeginFrozenSPEC(eachChild.getState().getDurationModeState()));
							eachChild.setChainingModeASAP(false);

						} else if (eachChild.getState() instanceof StateEndFrozenASAP) {
							eachChild.setState(new StateEndFrozenSPEC(eachChild.getState().getDurationModeState()));
							eachChild.setChainingModeASAP(false);

						}

						// stop the loop because one positive case is enough
						break;
					}
				}
			}

			// if not already updated
			if (!lstBeans.contains(eachChild)) {

				// shift it with its successors
				shiftTask(eachChild, offset);
			}

		}

	}

	/**
	 * Recursive method that updates every successor for the given task with the given offset
	 * 
	 * @param taskToUpdate
	 *            the reference task for which successors will be updated
	 * @param offset
	 *            the amount of time to offset successors (in hours)
	 */
	private void shiftTask(Task taskToUpdate, long offset) {

		if (!lstBeans.contains(taskToUpdate) && offset != 0) {

			taskToUpdate.getState().shift(taskToUpdate, offset);

			// if parent is in the list, no need to put each child into it
			if (!lstBeans.contains(taskToUpdate.getParent())) {
				lstBeans.add(taskToUpdate);
			}

			for (Task successor : taskToUpdate.getSuccessors()) {
				shiftTask(successor, offset);
			}
		}

		return;

	}

	/**
	 * <p>
	 * Sets the duration in hours for a phase according to its younger and older child
	 * </p>
	 */
	public void computePhaseDatesAndDuration(List<Task> lstChildren, Task phase) {
		// set younger and older child
		Task youngerChild = lstChildren.get(0);
		Task olderChild = lstChildren.get(0);
		for (Task eachChild : lstChildren) {
			if (olderChild.getEndDate().compareTo(eachChild.getEndDate()) <= 0) {
				olderChild = eachChild;
			}
		}

		// set begin and end dates
		phase.setBeginDate(youngerChild.getBeginDate());
		phase.setEndDate(olderChild.getEndDate());

		// set duration
		phase.setDuration((olderChild.getEndDate().getTime() - youngerChild.getBeginDate().getTime()) / TimeConstants.HOUR_IN_MILLIS);

		// if lstBeans isn't null, it means we're calling this method while updating children of the phase
		if (lstBeans != null) {
			lstBeans.add(phase);
		}
	}

}
